/*
 * This project has been written by
 * Corinna John (Hannover, Germany)
 * picturekey@binary-universe.net
 * on a rainy weekend.
 * 
 * You can use the code in any context you like,
 * as long as you do not delete this comment.
 * 
 * Please send me a little feedback about what you're
 * using this code for and what changes you'd like to
 * see in later versions. (And please excuse the bad english)
 * */

using System;
using System.Collections.Generic;
using System.IO;

namespace SteganoMidi
{
   

    /// <summary>Hide and extracts text in/from a MIDI file</summary>
    internal class MidiUtility
    {
        public delegate void EventOnError(string message);

        public event EventOnError OnError;

        private void Error(string message)
        {
            if (OnError != null)
            {
                OnError(message);
            }

        }
        //Reader for the source MIDI file
        private BinaryReader srcReader = null;
        //Writer for the destination MIDI file
        private BinaryWriter dstWriter = null;
        //Expected length of the extracted message
        private Int64 secretMessageLength = 0;
        //Count of 4-bit blocks to hide before a "Program Change" message
        private byte halfBytesPerMidiMessage = 0;
        //Has [halfBytesPerMidiMessage] already been written to/read from the file?
        private bool isHalfBytesPerMidiMessageFinshed = false;

        public byte HalfBytesPerMidiMessage
        {
            set { halfBytesPerMidiMessage = value; }
        }
        
        private void ReadMidiMessageHeader(ref MidiMessage midiMessage)
        {
            midiMessage = new MidiMessage();
            //Read time
            ReadVariableLengthValue(out midiMessage.Time);
            //Read type and channel
            midiMessage.MessageType = srcReader.ReadByte();
        }

        public static byte[] IntToArray(Int32 val)
        {
            byte[] bytes = new byte[4];

            bytes[0] = (byte)((val & 0xFF000000) >> 24);
            bytes[1] = (byte)((val & 0x00FF0000) >> 16);
            bytes[2] = (byte)((val & 0x0000FF00) >> 8);
            bytes[3] = (byte)(val & 0x000000FF);

            return bytes;
        }

        public static byte[] IntToArray(Int64 val)
        {
            byte[] bytes = new byte[8];
            for (int n = 0; n < 8; n++)
            {
                bytes[n] = (byte)(val >> (n * 8));
            }
            return bytes;
        }

        public Int64 ArrayToInt(byte[] bytes)
        {
            Int64 result = 0;
            for (int n = 0; n < bytes.Length; n++)
            {
                result += (bytes[n] << (n * 8));
            }
            return result;
        }

        private void WriteMidiMessage(MidiMessage midiMessage)
        {
            dstWriter.Write(midiMessage.Time);
            dstWriter.Write(midiMessage.MessageType);
            dstWriter.Write(midiMessage.MessageData);
        }

        private void ProcessMidiMessage(MidiMessage midiMessage, Stream secretMessage, Stream key, bool extract, ref bool isMessageComplete,
            ref int countIgnoreMessages, ref byte[] currentMessageByte, ref byte currentMessageByteIndex, ref int countBytesAdded)
        {

            if (extract)
            {
                ProcessMidiMessageE(midiMessage, secretMessage, key,
                    ref isMessageComplete, ref countIgnoreMessages,
                    ref currentMessageByte, ref currentMessageByteIndex,
                    ref countBytesAdded);
            }
            else
            {
                ProcessMidiMessageH(midiMessage, secretMessage, key,
                    ref isMessageComplete, ref countIgnoreMessages,
                    ref currentMessageByte, ref currentMessageByteIndex,
                    ref countBytesAdded);
            }
        }

        private void ProcessMidiMessageE(MidiMessage midiMessage, Stream secretMessage, Stream key, ref bool isMessageComplete,
            ref int countIgnoreMessages, ref byte[] currentMessageByte, ref byte currentMessageByteIndex, ref int countBytesAdded)
        {

            if ((countIgnoreMessages == 0) && (!isMessageComplete))
            {

                for (int n = 0; n < halfBytesPerMidiMessage; n++)
                {

                    ExtractHalfByte(midiMessage, secretMessage, ref currentMessageByte, ref currentMessageByteIndex, ref countBytesAdded);

                    if ((secretMessage.Length == 8) && (secretMessageLength == 0))
                    {
                        //The original secret message stream contained the size of the message
                        //in the first 8 bytes.
                        secretMessage.Seek(0, SeekOrigin.Begin);
                        byte[] bytes = new byte[8];
                        secretMessage.Read(bytes, 0, 8);
                        secretMessageLength = ArrayToInt(bytes);
                        secretMessage.SetLength(0);
                    }
                    else if ((secretMessageLength > 0) && (secretMessage.Length == secretMessageLength))
                    {
                        //All bytes extracted - ignore following "ProgramChange" messages
                        isMessageComplete = true;
                        break;
                    }

                    if ((n + 1) < halfBytesPerMidiMessage)
                    {
                        //There are more hidden packets following - read next header
                        ReadMidiMessageHeader(ref midiMessage);
                        midiMessage.MessageData = srcReader.ReadBytes(1);
                    }
                }

                //get next step width
                countIgnoreMessages = GetKeyByte(key);
                //copy non-message ProgramChange
                CopyMessage(midiMessage.MessageData.Length);

            }
            else
            {
                if (dstWriter != null)
                {
                    WriteMidiMessage(midiMessage);
                }
                countIgnoreMessages--;
            }
        }

        /// <summary>Hide blocks of half-bytes, copy the original Program Change event</summary>
        /// <param name="midiMessage">The original event</param>
        /// <param name="secretMessage">The complete message to hide</param>
        /// <param name="key">The key for ignoring events</param>
        /// <param name="isMessageComplete">Complete-flag</param>
        /// <param name="countIgnoreMessages">Count of messages already ignored according to [key]</param>
        /// <param name="currentMessageByte">The current byte from [secretMessage], split into high and low parts</param>
        /// <param name="currentMessageByteIndex">Index in [currentMessageByte]</param>
        /// <param name="countBytesAdded">Count of bytes added to the curren track</param>
        private void ProcessMidiMessageH(MidiMessage midiMessage, Stream secretMessage, Stream key, ref bool isMessageComplete,
            ref int countIgnoreMessages, ref byte[] currentMessageByte, ref byte currentMessageByteIndex, ref int countBytesAdded)
        {

            if (!isMessageComplete)
            {
                if (countIgnoreMessages == 0)
                {
                    //Hide as many 4-bit-packets as specified
                    for (int n = 0; n < halfBytesPerMidiMessage; n++)
                    {
                        //Create a new message with the same content as the original, initialize data byte
                        MidiMessage msg = new MidiMessage(midiMessage, new byte[midiMessage.MessageData.Length]);
                        //Write the new message to the destination file
                        isMessageComplete = HideHalfByte(msg, secretMessage,
                            ref currentMessageByte, ref currentMessageByteIndex, ref countBytesAdded);

                        if (isMessageComplete) { break; }
                    }
                    //get next step width
                    countIgnoreMessages = GetKeyByte(key);
                }
                else
                {
                    countIgnoreMessages--; //Count down to 0
                }
            }

            //copy original message
            WriteMidiMessage(midiMessage);
        }

        private void CopyMessage(int countDataBytes)
        {
            //copy unchanged message
            CopyVariableLengthValue();
            CopyByte(); //read type and channel
            CopyBytes(countDataBytes);
        }

        private bool HideHalfByte(MidiMessage midiMessage, Stream secretMessage, ref byte[] currentMessageByte, ref byte currentMessageByteIndex, ref int countBytesAdded)
        {
            bool returnValue = false;
            //Place the current byte of the secret message in the MIDI message's data byte
            midiMessage.MessageData[0] = currentMessageByte[currentMessageByteIndex];
            //Write it to destination file
            WriteMidiMessage(midiMessage);
            //Count the added bytes
            countBytesAdded += midiMessage.Time.Length + 1 + midiMessage.MessageData.Length;

            //Proceed to the next half-byte

            currentMessageByteIndex++;

            if (currentMessageByteIndex == 2)
            {
                int nextValue = secretMessage.ReadByte();
                if (nextValue < 0)
                {
                    returnValue = true;
                }
                else
                {
                    currentMessageByte = SplitByte((byte)nextValue);
                    currentMessageByteIndex = 0;
                }
            }

            return returnValue; //true if the secret message is finished
        }

        private void ExtractHalfByte(MidiMessage midiMessage, Stream secretMessage, ref byte[] currentMessageByte, ref byte currentMessageByteIndex, ref int countBytesAdded)
        {
            //Copy the hidden half-byte
            currentMessageByte[currentMessageByteIndex] = midiMessage.MessageData[0];

            //Count removed (negativly added) bytes: time, type, data
            countBytesAdded -= midiMessage.Time.Length + 1 + midiMessage.MessageData.Length;

            //Proceed to the next half-byte
            currentMessageByteIndex++;
            if (currentMessageByteIndex == 2)
            {
                //Write extracted byte
                byte completeMessageByte = (byte)((currentMessageByte[0] << 4) + currentMessageByte[1]);
                secretMessage.WriteByte(completeMessageByte);

                currentMessageByte[0] = 0;
                currentMessageByte[1] = 0;
                currentMessageByteIndex = 0;
            }
        }

        private byte[] SplitByte(byte b)
        {
            byte[] parts = new byte[2];
            parts[0] = (byte)(b >> 4); //shift higher half into lower half
            parts[1] = (byte)((byte)(b << 4) >> 4); //shift higher half outside, shift back
            return parts;
        }

        private int GetKeyByte(Stream key)
        {
            int countIgnore = key.ReadByte();
            if (countIgnore < 1)
            {
                key.Seek(0, SeekOrigin.Begin);
                countIgnore = key.ReadByte();
            }
            return countIgnore;
        }

        private char[] CopyChars(int count)
        {
            char[] chars = srcReader.ReadChars(count);
            if (dstWriter != null) { dstWriter.Write(chars); }
            return chars;
        }

        private byte[] CopyBytes(int count)
        {
            byte[] buffer = new byte[count];
            srcReader.Read(buffer, 0, count);
            if (dstWriter != null) { dstWriter.Write(buffer); }
            return buffer;
        }

        private byte CopyByte()
        {
            byte b = srcReader.ReadByte();
            if (dstWriter != null) { dstWriter.Write(b); }
            return b;
        }

        private long CopyVariableLengthValue()
        {
            byte[] dummy;
            return CopyVariableLengthValue(out dummy);
        }

        private long CopyVariableLengthValue(out byte[] rawDataBuffer)
        {
            long returnValue;
            byte b;
            List<byte> allBytes = new List<byte>();

            //read the first byte
            returnValue = CopyByte();
            allBytes.Add((byte)returnValue);

            if ((returnValue & 0x80) > 0)
            { //bit 7 is set: there are more bytes to read
                returnValue &= 0x7F; //remove bit 7 - it is only a not-the-last-one flag
                do
                {
                    b = CopyByte(); //read next byte
                    allBytes.Add(b);
                    //remove flag and append byte
                    returnValue = (returnValue << 7) + (b & 0x7F);
                } while ((b & 0x80) > 0); //until bit-7 is not set
            }

            rawDataBuffer = allBytes.ToArray();
            return returnValue;
        }

        private long ReadVariableLengthValue(out byte[] rawDataBuffer)
        {
            long returnValue;
            byte b;
            List<byte> allBytes = new List<byte>();

            //read the first byte
            returnValue = srcReader.ReadByte();
            allBytes.Add((byte)returnValue);

            if ((returnValue & 0x80) > 0)
            { //bit 7 is set: there are more bytes to read
                returnValue &= 0x7F; //remove bit 7 - it is only a not-the-last-one flag
                do
                {
                    b = srcReader.ReadByte(); //read next byte
                    allBytes.Add(b);
                    //remove flag and append byte
                    returnValue = (returnValue << 7) + (b & 0x7F);
                } while ((b & 0x80) > 0); //until bit-7 is not set
            }

            rawDataBuffer = allBytes.ToArray();
            return returnValue;
        }
    }
}
